package com.hcf.project.service.impl;

import cn.hutool.core.bean.BeanUtil;
import cn.hutool.core.date.DateUtil;
import cn.hutool.core.date.Week;
import cn.hutool.core.util.StrUtil;
import cn.hutool.http.HttpUtil;
import cn.hutool.json.JSONObject;
import cn.hutool.json.JSONUtil;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.hcf.project.common.constant.RedisCacheConstant;
import com.hcf.project.common.constant.ShortLinkConstant;
import com.hcf.project.common.convention.enums.ValidDateTypeEnum;
import com.hcf.project.common.convention.exception.ServiceException;
import com.hcf.project.dao.entity.*;
import com.hcf.project.dao.mapper.*;
import com.hcf.project.dto.req.ShortLinkCreateReqDTO;
import com.hcf.project.dto.req.ShortLinkPageReqDTO;
import com.hcf.project.dto.req.ShortLinkUpdateReqDTO;
import com.hcf.project.dto.resp.ListShortLinkCountRespDTO;
import com.hcf.project.dto.resp.ShortLinkCreateRespDTO;
import com.hcf.project.dto.resp.ShortLinkPageRespDTO;
import com.hcf.project.service.ShortLinkService;
import com.hcf.project.toolkit.HashUtil;
import com.hcf.project.toolkit.LinkUtil;
import jakarta.servlet.http.Cookie;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import org.jsoup.Jsoup;
import org.jsoup.nodes.Document;
import org.jsoup.select.Elements;
import org.redisson.api.RBloomFilter;
import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.dao.DuplicateKeyException;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.net.HttpURLConnection;
import java.net.URL;
import java.util.*;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicReference;

@Service
@Slf4j
public class ShortLinkServiceImpl extends ServiceImpl<LinkMapper, ShortLinkDO> implements ShortLinkService {

    @Autowired
    private RBloomFilter<String> shortLinkCreateCachePenetrationBloomFilter;
    @Autowired
    private LinkGoToMapper linkGoToMapper;
    @Autowired
    private StringRedisTemplate stringRedisTemplate;
    @Autowired
    private RedissonClient redissonClient;
    @Autowired
    private RBloomFilter<String> shortUriCreateCachePenetrationBloomFilter;
    @Autowired
    private LinkAccessStatsMapper linkAccessStatsMapper;
    @Autowired
    private LinkLocaleStatsMapper linkLocaleStatsMapper;
    @Autowired
    private LinkOsStatsMapper linkOsStatsMapper;
    @Autowired
    private LinkBrowserStatsMapper linkBrowserStatsMapper;
    @Autowired
    private LinkAccessLogsMapper linkAccessLogsMapper;
    @Autowired
    private LinkDeviceStatsMapper linkDeviceStatsMapper;
    @Autowired
    private LinkNetworkStatsMapper linkNetworkStatsMapper;
    @Value("${shortlink.stats.locale.key")
    private String mapKey;

    /**
     * 新建短链接
     * @param requestParam
     * @return
     */
    @Override
    public ShortLinkCreateRespDTO createShortLink(ShortLinkCreateReqDTO requestParam) {
        String suffix = generateSuffix(requestParam);
        ShortLinkDO shortLinkDO = BeanUtil.toBean(requestParam, ShortLinkDO.class);
        shortLinkDO.setShortUrl(suffix);
        shortLinkDO.setFullShortUrl(requestParam.getDomain()+"/"+suffix);
        shortLinkDO.setFavicon(getFavicon(requestParam.getOriginUrl()));
        ShortLinkGoToDO shortLinkGoToDO = ShortLinkGoToDO.builder()
                .fullShortUrl(shortLinkDO.getFullShortUrl())
                .gid(shortLinkDO.getGid())
                .build();
        try {
            shortUriCreateCachePenetrationBloomFilter.add(shortLinkDO.getFullShortUrl());
            baseMapper.insert(shortLinkDO);
            linkGoToMapper.insert(shortLinkGoToDO);
        }catch (DuplicateKeyException ex){
            log.info("短链接{}重复，无法插入", shortLinkDO.getFullShortUrl());
            throw new ServiceException(String.format("短链接%s重复，无法插入", shortLinkDO.getFullShortUrl()));
        }
        stringRedisTemplate.opsForValue()
                .set(RedisCacheConstant.GOTO_SHORT_LINK_KEY+shortLinkDO.getFullShortUrl(),
                shortLinkDO.getOriginUrl(),
                        LinkUtil.getLinkCacheValidTime(shortLinkDO.getValidDate()), TimeUnit.MILLISECONDS);
        shortLinkCreateCachePenetrationBloomFilter.add(shortLinkDO.getFullShortUrl());
        return ShortLinkCreateRespDTO.builder()
                .fullShortUrl(shortLinkDO.getFullShortUrl())
                .originUrl(shortLinkDO.getOriginUrl())
                .gid(shortLinkDO.getGid())
                .build();
    }


    /**
     * 查询短链接
     *
     * @param requestParam
     * @return
     */
    @Override
    public IPage<ShortLinkPageRespDTO> pageShortLink(ShortLinkPageReqDTO requestParam) {
        LambdaQueryWrapper<ShortLinkDO> wrapper = Wrappers.lambdaQuery(ShortLinkDO.class)
                .eq(ShortLinkDO::getGid, requestParam.getGid())
                .eq(ShortLinkDO::getDelFlag, 0)
                .eq(ShortLinkDO::getEnableStatus, 0);
        IPage<ShortLinkDO> resultPage = baseMapper.selectPage(requestParam, wrapper);
        return resultPage.convert(item -> BeanUtil.toBean(item, ShortLinkPageRespDTO.class));
    }


    /**
     * 统计分组内短链接总数
     * @param gids
     * @return
     */
    @Override
    public List<ListShortLinkCountRespDTO> listShortLinkCount(List<String> gids) {
        QueryWrapper<ShortLinkDO> queryWrapper = new QueryWrapper<>();
        queryWrapper.select("gid as gid", "count(*) as count")
                .eq("enable_status", 0)
                .in("gid", gids)
                .groupBy("gid");
        List<Map<String, Object>> result = baseMapper.selectMaps(queryWrapper);
        return BeanUtil.copyToList(result, ListShortLinkCountRespDTO.class);
    }


    /**
     * 更新短链接
     * @param requestParam
     * @return
     */
    @Override
    @Transactional(rollbackFor = Exception.class)
    public Void updateShortLink(ShortLinkUpdateReqDTO requestParam) {
        // 先查询
        LambdaQueryWrapper<ShortLinkDO> queryWrapper = Wrappers.lambdaQuery(ShortLinkDO.class)
                .eq(ShortLinkDO::getGid, requestParam.getGid())
                .eq(ShortLinkDO::getFullShortUrl, requestParam.getFullShortUrl())
                .eq(ShortLinkDO::getDelFlag, 0)
                .eq(ShortLinkDO::getEnableStatus, 0);
        ShortLinkDO hasShortLinkDO = baseMapper.selectOne(queryWrapper);
        // 有匹配记录
        if(hasShortLinkDO != null){
            ShortLinkDO shortLinkDO = ShortLinkDO.builder()
                    .shortUrl(hasShortLinkDO.getShortUrl())
                    .domain(hasShortLinkDO.getDomain())
                    .fullShortUrl(hasShortLinkDO.getFullShortUrl())
                    .clickNum(hasShortLinkDO.getClickNum())
                    .favicon(hasShortLinkDO.getFavicon())
                    .createdType(hasShortLinkDO.getCreatedType())
                    .gid(requestParam.getGid())
                    .originUrl(requestParam.getOriginUrl())
                    .validDate(hasShortLinkDO.getValidDate())
                    .validDateType(hasShortLinkDO.getValidDateType())
                    .describe(hasShortLinkDO.getDescribe())
                    .build();
            // gid一致进行更新
            if(Objects.equals(hasShortLinkDO.getGid(), requestParam.getGid())){
                LambdaUpdateWrapper<ShortLinkDO> updateWrapper = Wrappers.lambdaUpdate(ShortLinkDO.class)
                        .eq(ShortLinkDO::getGid, requestParam.getGid())
                        .eq(ShortLinkDO::getFullShortUrl, requestParam.getFullShortUrl())
                        .eq(ShortLinkDO::getDelFlag, 0)
                        .eq(ShortLinkDO::getEnableStatus, 0)
                        .set(Objects.equals(requestParam.getValidDateType(), ValidDateTypeEnum.PERMANENT.getValidDateType()), ShortLinkDO::getValidDate, null)
                        .set(ShortLinkDO::getEnableStatus, requestParam.getEnableStatus());
                baseMapper.update(shortLinkDO, updateWrapper);
            } else{
                // remove and set
                baseMapper.delete(Wrappers.lambdaQuery(ShortLinkDO.class)
                        .eq(ShortLinkDO::getGid, hasShortLinkDO.getGid())
                        .eq(ShortLinkDO::getFullShortUrl, requestParam.getFullShortUrl())
                        .eq(ShortLinkDO::getDelFlag, 0)
                        .eq(ShortLinkDO::getEnableStatus, 0));
                baseMapper.insert(shortLinkDO);
            }
        }else {
            throw new ServiceException("短链接不存在");
        }

        return null;
    }

    @SneakyThrows
    @Override
    public void redirect(String shortUri, HttpServletRequest request, HttpServletResponse response) {
        String fullShortUrl = request.getServerName() + "/" + shortUri;
        String originalUrl = stringRedisTemplate.opsForValue().get(RedisCacheConstant.GOTO_SHORT_LINK_KEY + fullShortUrl);
        if(originalUrl != null){
            shortLinkStats(fullShortUrl, null, request, response);
            response.sendRedirect(originalUrl);
            return;
        }
        // 访问数据不存在,查询布隆过滤器，如果不包含直接返回不查询数据库
        boolean contrains = shortLinkCreateCachePenetrationBloomFilter.contains(fullShortUrl);
        if(!contrains) {
            response.sendRedirect("/page/notfound");
            return;
        }
        // 是否有缓存的空值？
        String gotoIsNullShortLink = stringRedisTemplate.opsForValue().get(RedisCacheConstant.GOTO_IS_NULL_SHORT_LINK_KEY + fullShortUrl);
        if(gotoIsNullShortLink != null){
            response.sendRedirect("/page/notfound");
            return;
        }
        // 没有缓存的空值，则查询数据库
        RLock lock = redissonClient.getLock(RedisCacheConstant.LOCK_GOTO_LINK_KEY + fullShortUrl);
        lock.lock();
        try{
            // 二次查询缓存
            originalUrl = stringRedisTemplate.opsForValue().get(RedisCacheConstant.GOTO_SHORT_LINK_KEY + fullShortUrl);
            if(originalUrl != null){
                shortLinkStats(fullShortUrl, null, request, response);
                response.sendRedirect(originalUrl);
                return;
            }
            ShortLinkGoToDO shortLinkGoTo = linkGoToMapper.selectOne( Wrappers.lambdaQuery(ShortLinkGoToDO.class)
                    .eq(ShortLinkGoToDO::getFullShortUrl, fullShortUrl));
            if(shortLinkGoTo == null){
                // 数据不存在，缓存空值
                stringRedisTemplate.opsForValue().set(RedisCacheConstant.GOTO_IS_NULL_SHORT_LINK_KEY + fullShortUrl, "-", 30, TimeUnit.MINUTES);
                response.sendRedirect("/page/notfound");
                return;
            }
            String gid = shortLinkGoTo.getGid();
            ShortLinkDO shortLink = baseMapper.selectOne(Wrappers.lambdaQuery(ShortLinkDO.class)
                    .eq(ShortLinkDO::getGid, gid));
            if(shortLink != null){
                // 判断有效期，是否过期
                if(shortLink.getValidDate() != null && shortLink.getValidDate().before(new Date())){
                    // 短链接已过期，按缓存空值处理
                    stringRedisTemplate.opsForValue().set(RedisCacheConstant.GOTO_IS_NULL_SHORT_LINK_KEY + fullShortUrl, "-", 30, TimeUnit.MINUTES);
                    response.sendRedirect("/page/notfound");
                    return;
                }
                stringRedisTemplate.opsForValue()
                        .set(RedisCacheConstant.GOTO_SHORT_LINK_KEY+fullShortUrl, shortLink.getOriginUrl(),
                                LinkUtil.getLinkCacheValidTime(shortLink.getValidDate()), TimeUnit.MILLISECONDS);
                shortLinkStats(fullShortUrl, gid, request, response);
                response.sendRedirect(shortLink.getOriginUrl());
            }
        }finally {
            lock.unlock();
        }

    }



    private void shortLinkStats(String fullShortUrl, String gid, HttpServletRequest request, HttpServletResponse response){
        AtomicBoolean isNew = new AtomicBoolean();
        AtomicBoolean isNewIp = new AtomicBoolean();
        Cookie[] cookie = request.getCookies();
        try {
            AtomicReference<String> uv = new AtomicReference<>();
            Runnable addResponseCookie = () -> {
                uv.set(UUID.randomUUID().toString());
                Cookie uvcookie = new Cookie("uv", uv.get());
                uvcookie.setPath(StrUtil.sub(fullShortUrl, fullShortUrl.lastIndexOf("/"), fullShortUrl.length()));
                uvcookie.setMaxAge(60 * 60 * 24 * 30);
                response.addCookie(uvcookie);
                isNew.set(Boolean.TRUE);
            };
            // 判断cookie是否为空，不为空提取uv字段，验证是否已存在redis中，不存在则将isNew设置为true
            if(cookie != null && cookie.length > 0){
                Arrays.stream(cookie)
                        .filter(each -> StrUtil.equals(each.getName(), "uv"))
                        .findFirst()
                        .map(Cookie::getValue)
                        .ifPresentOrElse(each -> {
                            uv.set(each);
                            Long uvAdded = stringRedisTemplate.opsForSet().add("short-link:stats:uv:" + fullShortUrl, each);
                            isNew.set(uvAdded > 0);
                        }, addResponseCookie);
            }else {
                addResponseCookie.run();
            }
            String uip = request.getRemoteAddr();
            Long uipAdded = stringRedisTemplate.opsForSet().add("short-link:stats:uip:" + fullShortUrl, uip);
            isNewIp.set(uipAdded > 0);
            // 如果没有传入gid，则查询数据库获取短链接id，否则直接使用传入参数的gid
            if(StrUtil.isBlank(gid)){
                LambdaQueryWrapper<ShortLinkGoToDO> eq = Wrappers.lambdaQuery(ShortLinkGoToDO.class)
                        .eq(ShortLinkGoToDO::getFullShortUrl, fullShortUrl);
                ShortLinkGoToDO shortLinkGoToDO = linkGoToMapper.selectOne(eq);
                gid = shortLinkGoToDO.getGid();
            }
            int hour = DateUtil.hour(new Date(), true);
            Week week = DateUtil.dayOfWeekEnum(new Date());
            int weekValue = week.getIso8601Value();
            LinkAccessStatsDO linkAccessStatsDO = LinkAccessStatsDO.builder()
                    .pv(1)
                    .uv(isNew.get() ? 1 : 0)
                    .uip(isNewIp.get() ? 1 : 0)
                    .fullShortUrl(fullShortUrl)
                    .gid(gid)
                    .date(new Date())
                    .hour(hour)
                    .weekday(weekValue)
                    .build();
            linkAccessStatsMapper.linkAccessStats(linkAccessStatsDO);
            // 统计地域信息
            Map<String, Object> localeParams = new HashMap<>();
            localeParams.put("ip", request.getRemoteAddr());
            localeParams.put("key", mapKey);
            String mapAPIRet = HttpUtil.get(ShortLinkConstant.IP_API_URL, localeParams);
            JSONObject localeInfo = JSONUtil.parseObj(mapAPIRet);
            String infoCode = localeInfo.getStr("infoCode");
            // 验证请求是否成功
            if(infoCode != null && infoCode.equals(10000)){
                String province = localeInfo.getStr("province");
                boolean success = province.equals("[]");
                LinkLocaleStatsDO linkLocaleStatsDO = LinkLocaleStatsDO.builder()
                        .fullShortUrl(fullShortUrl)
                        .gid(gid)
                        .city(success ? localeInfo.getStr("city") : "未知城市")
                        .province(success ? localeInfo.getStr("province") : "未知省份")
                        .country(success ? localeInfo.getStr("country") : "未知国家")
                        .adcode(success ? localeInfo.getStr("adcode") : "未知区域编码")
                        .cnt(1)
                        .date(new Date())
                        .build();
                linkLocaleStatsMapper.linkLocaleStats(linkLocaleStatsDO);
                String os = LinkUtil.getOs(request);
                LinkOsStatsDO linkOsStatsDO = LinkOsStatsDO.builder()
                        .gid(gid)
                        .fullShortUrl(fullShortUrl)
                        .cnt(1)
                        .os(os)
                        .date(new Date())
                        .build();
                linkOsStatsMapper.linkOsStats(linkOsStatsDO);
                String browser = LinkUtil.getBrowser(request);
                LinkBrowserStatsDO linkBrowserStatsDO = LinkBrowserStatsDO.builder()
                        .browser(browser)
                        .cnt(1)
                        .gid(gid)
                        .fullShortUrl(fullShortUrl)
                        .date(new Date())
                        .build();
                linkBrowserStatsMapper.linkBrowserStats(linkBrowserStatsDO);
                LinkAccessLogsDO linkAccessLogsDO = LinkAccessLogsDO.builder()
                        .user(uv.get())
                        .ip(request.getRemoteAddr())
                        .browser(browser)
                        .os(os)
                        .gid(gid)
                        .fullShortUrl(fullShortUrl)
                        .build();
                linkAccessLogsMapper.insert(linkAccessLogsDO);
                LinkDeviceStatsDO linkDeviceStatsDO = LinkDeviceStatsDO.builder()
                        .device(LinkUtil.getDevice(request))
                        .cnt(1)
                        .gid(gid)
                        .fullShortUrl(fullShortUrl)
                        .date(new Date())
                        .build();
                linkDeviceStatsMapper.linkDeviceStats(linkDeviceStatsDO);
                LinkNetworkStatsDO linkNetworkStatsDO = LinkNetworkStatsDO.builder()
                        .network(LinkUtil.getNetwork(request))
                        .cnt(1)
                        .gid(gid)
                        .fullShortUrl(fullShortUrl)
                        .date(new Date())
                        .build();
                linkNetworkStatsMapper.shortLinkNetworkState(linkNetworkStatsDO);
            }
        }catch (Throwable ex){
            log.error("短链接访问量统计异常", ex);
        }
    }

    public String generateSuffix(ShortLinkCreateReqDTO requestParam){
        String originUrl = requestParam.getOriginUrl();
        String shortLinkSuffix, fullShortLink;
        int retryNum = 0;
        while(true) {
            if(retryNum >= 10) {
                throw new ServiceException("生成短链接重试操作频繁，请稍后再试");
            }

            shortLinkSuffix = HashUtil.hashToBase62(originUrl + System.currentTimeMillis());
            fullShortLink = requestParam.getDomain() + "/" + shortLinkSuffix;
            if (!shortLinkCreateCachePenetrationBloomFilter.contains(fullShortLink)) {
                break;
            }
            retryNum++;
        }

        return shortLinkSuffix;
    }


    @SneakyThrows
    private String getFavicon(String url){
        URL targetUrl = new URL(url);
        HttpURLConnection connection = (HttpURLConnection) targetUrl.openConnection();
        connection.setRequestMethod("GET");
        connection.connect();

        int responseCode = connection.getResponseCode();
        if(responseCode == HttpURLConnection.HTTP_OK) {
            Document document = Jsoup.connect(url).get();
            Elements faviconLink = document.select("link[rel=icon]");
            return faviconLink != null ? faviconLink.attr("abs:href") : null;
        }
        return null;
    }
}
